## Слоты

Существует специальная коллекция __slots__, которую можно прописывать в любом классе языка Python.

Для простоты восприятия информации возьмем простой класс для представления точки на плоскости:

```python
class Point:

    def __init__(self, x, y):
        self.x = x
        self.y = y




class Point2D:
    __slots__ = ('x', 'y')
    max_x = 100

    def __init__(self, x, y):
        self.x = x
        self.y = y

# Пока в этом классе нет коллекции __slots__, поэтому при создании его экземпляра можно свободно оперировать существующими свойствами

p1 = Point(1, 2)
print (p1.x)
p1.y = 100
p1.z = 200


p2 = Point2D(1, 2) # И создавать новые:
print(p1.__dict__) 

p2.x
p2.y = 100

print(p2.max_x)
# p2.z = 4 # нет

# print(p2.__dict__) # нет !
print(p2.__slots__)
```


Но что, если мы хотим объявить класс точки на плоскости, чтобы у его экземпляров были только два свойства x и y и никакие другие. 

Как это сделать? Для этого, как раз и применяется коллекция `__slots__`.

Давайте я создам для сравнения еще один класс, в котором укажу эту коллекцию:

```python
class Point2D:
    __slots__ = ('x', 'y')
    max_x = 100

```

мы в кортеже перечисляем атрибуты с именами `x` и `y` (указываются в виде строки) и только такие локальные свойства могут существовать в экземплярах этого класса. 

Проверим это. Создадим экземпляр:

> pt2 = Point2D(10, 20)
Выведем свойства x и y, убедимся, что они присутствуют в экземпляре:

```python
pt2.x
pt2.y
```
Но вот добавить новое уже не получится:

> pt2.z = 30

Это произошло как раз по тому, что в классе прописана коллекция __slots__ и мы ее можем даже вывести:

> pt2.__slots__

А вот привычная нам коллекция

>pt2.__dict__

будет отсутствовать. 
То есть, класс и его экземпляры ведут себя немного по-другому. 

Мы совершенно спокойно можем изменять, удалять и добавлять локальные свойства x, y:

```python
pt2.x = 50
del pt2.y
pt2.y = 100
```
Но только их и никакие другие. 



Причем, обратите внимание, речь идет только о локальных свойствах экземпляров. В сам класс мы по-прежнему можем добавлять любые атрибуты, например `max_x`.
```python
# экономия памяти
pt1.__sizeof__()
pt2.__sizeof__()

```

И еще коллекция `__slots__` ускоряет работу с локальными свойствами экземпляров класса. Например, если добавить в каждый класс метод:
```python
    def calc(self):
        self.x += 1
        del self.y 
        self.y = 0

# И замерить скорость его работы:

t1 = timeit.timeit(pt.calc)
t2 = timeit.timeit(pt2.calc)

print(t1, t2)

```

То мы увидим, что `t1` больше, чем `t2`, то есть, класс `Point2D` работает быстрее с локальными свойствами, чем класс `Point`.

Вот такие три особенности дает коллекция `__slots__ `экземплярам класса:

* ограничение создаваемых локальных свойств;
* уменьшение занимаемой памяти;
* ускорение работы с локальными свойствами.

### продолжение

Как мы говорили коллекция `__slots__` позволяет ограничивать список локальных свойств в экземплярах класса, в котором прописана. 

Например, для класса точки на плоскости мы разрешали только свойства с именами x и y:

> __slots__ = ('x', 'y', 'length')

```python
class Point2D:
    __slots__ = ('x', 'y', 'length')
 
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.length = (x*x + y*y) ** 0.5

```
И, далее, создавая экземпляр этого класса:

> pt = Point2D(1, 2)

автоматически получаем свойство length с расстоянием до точки (относительно нуля):

> pt.length

Но что если в этом классе прописать свойство (property) с тем же самым именем length? 

А в коллекции `__slots__` задать приватное свойство: `__length`

> __slots__ = ('x', 'y', '__length')

Давайте добавим это свойство и посмотрим, что получится:

```python
    @property
    def length(self):
        return self.__length

    @length.setter
    def length(self, value):
        self.__length = value

```


И, затем, после создания экземпляра класса, мы можем использовать такой синтаксис:

```python
pt = Point2D(3, 4)
print(pt.length)

```

Смотрите, мы здесь обращаемся к свойству `length`, хотя в `__slots__` нет такого разрешенного имени.

И при этом нет никаких ошибок выполнения. 

В принципе, так и должно быть, потому что `length` – это не локальная переменная экземпляра класса, а атрибут класса Point2D. 

Коллекция `__slots__` не накладывает ограничения на атрибуты класса, только на локальные атрибуты его экземпляров. 

Благодаря этому и появляется свойство `length`, которое работает как геттер и сеттер класса `Point2D`.


### Поведение __slots__ при наследовании классов


Следующий важный момент – это особенности работы коллекции __slots__ при наследовании классов. 

Пример, чтобы показать первую особенность этой коллекции. Допустим, мы создаем еще один класс Point3D, который наследует от класса Point2D и он пока будет пустым:

```python
class Point3D(Point2D):
    pass
```


Тогда, для экземпляров этого класса будет разрешено создание локальных свойств, несмотря на то, что в базовом классе присутствует коллекция `__slots__`:

```python
pt3 = Point3D(10, 20)
pt3.z = 30
```

То есть, по умолчанию эта коллекция не наследуется дочерними классами и они ведут себя как обычные, без каких-либо ограничений, сохраняя все локальные свойства, используя коллекцию:
> pt3.__dict__

Однако, если мы ее пропишем даже без указания каких-либо элементов:

```python
class Point3D(Point2D):
    __slots__ = ()
```

то ограничения вступят в свои права и она будет как бы унаследована от базового класса Point2D.

То есть, в классе Point3D сейчас доступны только два локальных свойства x и y.

Но нам нужно разрешить еще одно имя z. Для этого достаточно одно его и прописать в этой коллекции:

```python
class Point3D(Point2D):
    __slots__ = 'z',
```

Тогда имена x, y будут наследоваться от класса Point2D, а z – добавляться классом Point3D. 

Также обратите внимание на висячую запятую. 

Она означает, что z – это элемент кортежа, а не просто строка. 
Предпочтительно записывать в таком виде. Хотя, и без запятой тоже все будет работать.

Очевидно, следующим шагом надо добавить конструктор в дочерний класс. Запишем его в следующем виде:

```python

class Point3D(Point2D):
    __slots__ = 'z',
 
    def __init__(self, x, y, z):
        super().__init__(x, y)
        self.z = z
```

И экземпляр будем создавать уже с тремя параметрами:

> pt3 = Point3D(10, 20, 30)


Теперь, наш дочерний класс содержит уникальный код, расширяющий функционал базового класса без каких-либо повторений. 

При этом, в обоих классах используется ограничение, но с разным набором локальных свойств.